// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: April 16 2010 

#region

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Salient.Net;

#endregion

namespace Salient.Web.Hosting
{
    /// <summary>
    /// <para>A general purpose Visual Studio 2008 Development Server (WebDev.WebServer.exe) test fixture.</para>
    /// <para>Controls a shelled instance of WebDev.WebServer.exe so F5 debugging is not possible with this server.</para>
 /// </summary>
    public class WebDevServer:IWebServer
    {
        ///<summary>
        ///</summary>
        public WebDevServer()
        {
        }

        ///<summary>
        ///</summary>
        public string ApplicationPath { get; private set; }
        ///<summary>
        ///</summary>
        public string HostName { get; private set; }
        ///<summary>
        ///</summary>
        public int Port { get; private set; }
        ///<summary>
        ///</summary>
        public string VirtualPath { get; private set; }

        ///<summary>
        ///</summary>
        public string RootUrl
        {
            get { return string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}{2}", HostName, Port, VirtualPath); }
        }

        private int ProcessId { get; set; }

        private static string ProgramFilesx86
        {
            get
            {
                if (8 == IntPtr.Size ||
                    (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"))))
                {
                    return Environment.GetEnvironmentVariable("ProgramFiles(x86)");
                }

                return Environment.GetEnvironmentVariable("ProgramFiles");
            }
        }


        /// <summary>
        /// Combine the RootUrl of the running web application with the relative url
        /// specified.
        /// </summary>
        /// <param name="relativeUrl"></param>
        /// <returns></returns>
        public string NormalizeUrl(string relativeUrl)
        {
            return NetworkUtils.NormalizeUrl(RootUrl, relativeUrl);
        }

        /// <summary>
        /// <para>Starts an instance of WebDev.WebServer.exe on first available port in the range 8000-10000.</para>
        /// </summary>
        /// <remarks>
        /// <para>For use with testing frameworks, you would typically call this method in the Fixture setup method.</para>
        /// </remarks>
        /// <param name="applicationPath"></param>
        public void StartServer(string applicationPath)
        {
            StartServer(applicationPath, NetworkUtils.GetAvailablePort(8000, 10000, IPAddress.Loopback, true), "/", "localhost");
        }

        /// <summary>
        /// <para>Starts an instance of WebDev.WebServer.exe using the supplied parameters.</para>
        /// </summary>
        /// <remarks>
        /// <para>For use with testing frameworks, you would typically call this method in the Fixture setup method.</para>
        /// </remarks>
        /// <param name="applicationPath">Physical path to application.</param>
        /// <param name="port">Port to listen on.</param>
        /// <param name="virtualPath">Optional. defaults to "/"</param>
        /// <param name="hostName">Optional. Is used to construct RootUrl. Defaults to "localhost"</param>
        public void StartServer(string applicationPath, int port, string virtualPath, string hostName)
        {
            applicationPath = Path.GetFullPath(applicationPath);
            hostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
            virtualPath = String.Format("/{0}/", (virtualPath ?? string.Empty).Trim('/')).Replace("//", "/");

            if (GetRunningInstance(applicationPath, port, virtualPath) != null)
            {
                return;
            }

            IPAddress ipAddress = IPAddress.Loopback;

            if (!NetworkUtils.IsPortAvailable(ipAddress, port))
            {
                throw new Exception(string.Format("Port {0} is in use.", port));
            }

            string arguments = String.Format(CultureInfo.InvariantCulture, "/port:{0} /path:\"{1}\" /vpath:\"{2}\"",
                                             port, applicationPath, virtualPath);

            using (Process proc = new Process())
            {
                proc.StartInfo = new ProcessStartInfo
                {
                    FileName = GetWebDevExecutablePath(),
                    Arguments = arguments,
                    CreateNoWindow = true
                };
                bool started = proc.Start();
                if (!started)
                {
                    throw new Exception("Error starting server");
                }
                ProcessId = proc.Id;
            }

            ApplicationPath = applicationPath;
            Port = port;
            VirtualPath = virtualPath;
            HostName = hostName;
        }

        /// <summary>
        /// This overload is invalid for WebDevServer. WebDevServer can only serve on LoopBack.
        /// </summary>
        public void StartServer(string applicationPath, IPAddress ipAddress, int port, string virtualPath, string hostname)
        {
            throw new NotImplementedException("This overload is invalid for WebDevServer. WebDevServer can only serve on LoopBack.");
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

        /// <summary>
        /// a full list of constants can be found here:
        /// http://www.vbcode.com/Asp/showsn.asp?theID=11797
        /// </summary>
        public const int WM_QUERYENDSESSION = 0x11;

        /// <summary>
        /// After stopping an instance of WebDevWebServer.exe, the orphaned tray notification icon will remain. 
        /// This can be annoying but simply running your mouse over them will dispose of it. 
        /// I think there may be some Win32 voodoo to deal with this but is a very low priority right now.
        /// </summary>
        /// <remarks>
        /// <para>For use with testing frameworks, you would typically call this method in the Fixture teardown method.</para>
        /// <para>If not called explicitely, Dispose will shut down the instance.</para>
        /// </remarks>
        public void StopServer()
        {
            try
            {
                WebDevServer instance = GetRunningInstance(ApplicationPath, Port, VirtualPath);
                if (instance != null)
                {
                    using (Process process = Process.GetProcessById(instance.ProcessId))
                    {
                        if (process.MainWindowHandle != IntPtr.Zero)
                        {
                            SendMessage(process.MainWindowHandle, WM_QUERYENDSESSION, IntPtr.Zero, IntPtr.Zero);
                        }
                        //process.Kill();

                        // allow the port time to be released
                        process.WaitForExit(1000);
                    }
                }
            }

                // ReSharper disable EmptyGeneralCatchClause
            catch
            // ReSharper restore EmptyGeneralCatchClause
            {
                // what can we do about spurious exceptions on Process.Kill?
            }
        }

        ///<summary>
        /// Closes the server instance, if any.
        ///</summary>
        public void Dispose()
        {
            StopServer();
        }

        #region Instance Management


        private const string WebDevPath = @"Common Files\Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";
        private static readonly Regex RxPath = new Regex(@"/path:""(?<path>[^""]*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxPort = new Regex(@"/port:(?<port>\d*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxVPath = new Regex(@"/vpath:""(?<vpath>[^""]*)", RegexOptions.ExplicitCapture);

        /// <summary>
        /// Private ctor to build from WMI query
        /// </summary>
        private WebDevServer(string commandLine, int processId)
        {
            ProcessId = processId;
            Port = Int32.Parse(RxPort.Match(commandLine).Groups["port"].Value);
            ApplicationPath = RxPath.Match(commandLine).Groups["path"].Value;
            VirtualPath =
                String.Format("/{0}/", RxVPath.Match(commandLine).Groups["vpath"].Value.Trim('/')).Replace("//", "/");
            HostName = "localhost";
        }

        ///<summary>
        ///</summary>
        ///<param name="applicationPath"></param>
        ///<param name="port"></param>
        ///<param name="virtualPath"></param>
        ///<returns></returns>
        public static WebDevServer GetRunningInstance(string applicationPath, int port, string virtualPath)
        {
            return GetRunningInstances().FirstOrDefault(s =>
                string.Compare(s.ApplicationPath, applicationPath, StringComparison.OrdinalIgnoreCase) == 0 &&
                string.Compare(s.VirtualPath, virtualPath, StringComparison.OrdinalIgnoreCase) == 0 &&
                s.Port == port);
        }

        /// <summary>
        /// Queries WMI and builds a list of WebDevWebServer representing all running instances.
        /// </summary>
        public static List<WebDevServer> GetRunningInstances()
        {
            List<WebDevServer> returnValue = new List<WebDevServer>();
            const string query = "select CommandLine,ProcessId from Win32_Process where Name='WebDev.WebServer.EXE'";
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
            {
                using (ManagementObjectCollection results = searcher.Get())
                {
                    foreach (ManagementObject process in results)
                    {
                        returnValue.Add(
                            new WebDevServer(process["CommandLine"].ToString(), int.Parse(process["ProcessId"].ToString())));
                        process.Dispose();
                    }
                }
            }
            return returnValue;
        }

        ///<summary>
        ///</summary>
        ///<returns></returns>
        ///<exception cref="FileNotFoundException"></exception>
        public static string GetWebDevExecutablePath()
        {
            string exePath = Path.Combine(ProgramFilesx86, WebDevPath);

            if (!File.Exists(exePath))
            {
                throw new FileNotFoundException(exePath);
            }
            return exePath;
        }

 

        #endregion
    }
}